package com.mall.common;

import com.alibaba.druid.support.json.JSONUtils;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Slf4j
@Component
public class RedisClient {
    @Autowired
    RedisTemplate redisTemplate;

    private static final ExecutorService executorService= Executors.newFixedThreadPool(10);

    //封装一个设置存缓存的方法（设置过期时间）
    public void set(String key, Object value, Long time, TimeUnit unit){
        redisTemplate.opsForValue().set(key,value,time,unit);
    }
    //封装一个设置为逻辑过期的缓存的方法
    public void setWithLogicExpire(String key,Object value,Long time,TimeUnit unit){
        RedisData<Object> redisData = new RedisData<>();
        //封装缓存数据信息-
        redisData.setData(value);
        //封装逻辑过期时间
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        redisTemplate.opsForValue().set(key,redisData);
    }
    //设置防止缓存穿透查询
    public <T,ID> T selectWithPassThrough(String keyPrefix, ID id,Class<T> type,Function<ID,T> function,Long time,TimeUnit unit){
        //根据前缀+id生产一个key
        String key=keyPrefix+id;
        //首先查询缓存，看是否命中
        T  t= (T) redisTemplate.opsForValue().get(key);
        if (ObjectUtils.isNotEmpty(t)){
            return t;//如果有数据直接返回
        }
        if (t.equals("")){//数据不为空，但是是空字符串就是为了防止缓存穿透设置的空字符串
            return null;
        }
        //如果没有则去数据库里查
        T apply = function.apply(id);
        if (apply==null){
            //如果数据库查询结果为空，则设置一个空数据为value防止缓存穿透,过期设置时间较短防止这个id真的有数据了
            redisTemplate.opsForValue().set(key,"",2,TimeUnit.MINUTES);
            return null;
        }
        //将数据存放到数据库
        redisTemplate.opsForValue().set(key,apply,time,unit);
        //存放后返回数据
        return apply;
    }
    //设置逻辑过期时间防止缓存击穿
    public <T,ID> T selectWithBreakDown(String keyPrefix, ID id,Class<T> type,Function<ID,T> function,Long time,TimeUnit unit){
        //根据前缀+id生产一个key
        String key=keyPrefix+id;
        //由于设置的逻辑过期时间所以缓存一定命中，获取数据字段信息来判断是否过期
        T t = (T) redisTemplate.opsForValue().get(key);
        RedisData<T> redisData= (RedisData<T>) t;
        LocalDateTime expireTime = redisData.getExpireTime();
        //如果没过期
        if (expireTime.isAfter(LocalDateTime.now())){
            //直接返回数据
            return redisData.getData();
        }
        String lockKey="lock:"+id;
        //获得锁
        Boolean lock = tryLock(lockKey);
        //如果过期了则单独创建一个线程获得锁去执行更新操作
        if (lock) {
            //开启一个线程去更新过期的热点key对应的数据
            executorService.execute(()->{
                try {
                    //查询数据并更新
                    T apply = function.apply(id);
                    setWithLogicExpire(key,apply,5L,TimeUnit.MINUTES);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    unlock(lockKey);
                }
            });
            //直接返回信息（不管是不是旧数据）
            return redisData.getData();
        }
        //直接返回信息（不管是不是旧数据）
        return redisData.getData();
    }
    private Boolean tryLock(String key) {
        //自定义一个锁
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return flag;
    }

    private void unlock(String key) {
        redisTemplate.delete(key);
    }
}

